Skip to content

[fix] Event Strength of Schedule crash and Simulation EPA column - #8

Closed
chondl wants to merge 2 commits into
masterfrom
sos-sim-fix
Closed

[fix] Event Strength of Schedule crash and Simulation EPA column#8
chondl wants to merge 2 commits into
masterfrom
sos-sim-fix

Conversation

@chondl

@chondl chondl commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Track 1 · standalone bug fix. Off master (a2cea55). No dependencies; order-independent relative to the other bug fixes. Recommended slot: 3 of 5 in Track 1 (#5 → #12 → #8 → #6 → #1). No companion tests. Conflict-free against every other open PR.


Two fixes to the event page's live-simulation feature, both surfaced by
rigorously testing the Strength of Schedule and Simulation tabs against
several 2026 events. Both bugs are pre-existing: worker.ts and simulation.tsx
are unchanged on every branch relative to master.

1. Strength of Schedule renders blank (or NaN) when pre-event EPAs are identical

Symptom. On the Strength of Schedule tab the RP/Rank/EPA/Composite score
columns fail to populate. Two observed manifestations across events:

  • score columns entirely blank;
  • NaN in EPA Score and Composite Score (RP/Rank Score present).

Root cause. strengthOfSchedule() calls _strengthOfSchedule(data, simCount, false) (the "Before Event" pass) first. That pass computes epaSd, the standard
deviation of every team's pre-event start EPA, and builds a Gaussian to score
EPA-based schedule strength:

const distrib = Gaussian(0, (epaSd * epaSd * 5) / N);

Early in a season — before ratings diverge — every team at an event shares the
same cold-start EPA, so epaSd is 0, the variance is 0, and gaussian
throws Error('Variance must be > 0'). Because the worker's message handler
calls strengthOfSchedule(...) without await/catch, the throw becomes a
silent unhandled promise rejection inside the worker: no message is posted and
the table stays blank. A floating-point variant produces the NaN symptom —
with all EPAs equal, Math.sqrt(sum(x^2)/n - avg^2) occasionally takes the root
of a tiny negative rounding residue, so epaSd is NaN, gaussian does not
throw (NaN <= 0 is false), and every EPA percentile is NaN. Both are the same
degeneracy: EPA carries no schedule signal when all ratings are identical.

Fix. Floor the variance with || 1e-9, which treats both 0 and NaN as
falsy and substitutes a negligible positive variance; with deltaEPA == 0 the
CDF at the mean is 0.5, so every team gets a neutral 0.5 EPA percentile — the
correct answer when EPA is non-informative. Any real positive variance passes
through untouched, so events with diverged ratings are unaffected. Matches the
|| 0 fallback idiom already used throughout this worker.

2. Simulation tab shows EPA 0 for every team

Symptom. On the Simulation tab the EPA column reads 0 for all teams
(the predicted ranks, RP means, and percentiles are correct).

Root cause. simulation.tsx reads the team EPA as
teamEvent.epa.total_points.mean. That matches the APITeamEvent TypeScript
type, but not the runtime shape: the backend serves epa.total_points as a plain
number, so .mean is undefined and ?? 0 renders 0.

Fix. Read epa.breakdown.total_points — the same field the SOS tab and the
simulation worker already use — which is present at runtime.

Verification

Reproduced and confirmed fixed against completed 2026 events:

  • epaSd == 0 event: before — Gaussian throws, SOS blank; after — all score columns populate (EPA Score 0.5).
  • epaSd == NaN (float rounding) event: before — EPA/Composite NaN; after — populate (EPA Score 0.5).
  • event with a genuinely non-zero EPA spread: EPA percentile 0.387 before and after — unchanged (fix is inert on the non-degenerate path).
  • Simulation EPA column: 0 before, real per-team EPA after; predicted-rank distributions unchanged and still vary run-to-run.

chondl added 2 commits July 10, 2026 08:07
Floor the EPA-percentile Gaussian variance so it is always > 0.

When every team at an event shares the same pre-event start EPA (true for
all events early in a season, before ratings diverge), epaSd is 0, so the
variance (epaSd^2 * 5 / N) is 0 -- or NaN when floating-point error makes
the variance argument to Math.sqrt slightly negative. gaussian() throws on
variance <= 0, which rejected the un-awaited strengthOfSchedule() promise in
the worker and left the SOS table blank; the NaN path instead rendered NaN in
the EPA/Composite columns. Both are the same degeneracy. '|| 1e-9' treats 0
and NaN alike, yielding a neutral 0.5 EPA percentile, and leaves every real
positive variance untouched.
Read the team EPA from epa.breakdown.total_points, the field the SOS tab and
the simulation worker already use. simulation.tsx read epa.total_points.mean,
which matches the stale APITeamEvent type but not the runtime shape: the backend
serves epa.total_points as a plain number, so .mean is undefined and every row
rendered EPA 0.
@chondl chondl changed the title Fix Strength of Schedule crash when pre-event EPAs are identical Fix event Strength of Schedule crash and Simulation EPA column Jul 10, 2026
@chondl chondl changed the title Fix event Strength of Schedule crash and Simulation EPA column [fix] Event Strength of Schedule crash and Simulation EPA column Jul 10, 2026
@chondl

chondl commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

Superseded — both fixes (SOS variance floor, Simulation EPA field) relocated to #5. Branch kept.

@chondl chondl closed this Jul 10, 2026
@chondl
chondl deleted the sos-sim-fix branch July 19, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant